#ifndef SIGNALANDSLOT_H
#define SIGNALANDSLOT_H

#include <vector>
#include <memory>
#include <functional>

namespace NahidaProject {
	template <typename...> class __declspec(dllexport) Signal;

	namespace SignalAndSlot {
		class __declspec(dllexport) Connection {
		public:
			template <typename...> friend class ::NahidaProject::Signal;

			Connection() noexcept = default;
			Connection(const Connection&) = default;
			Connection(Connection&&) noexcept = default;

			Connection& operator=(const Connection&) = default;
			Connection& operator=(Connection&&) noexcept = default;

			void Disconnect();
			bool Connected() const;

		private:
			class __declspec(dllexport) SignalInterface {
			protected:
				friend class Connection;

				SignalInterface() = default;
				~SignalInterface() = default;

			private:
				SignalInterface(const SignalInterface&) = delete;
				SignalInterface& operator=(const SignalInterface&) = delete;

				virtual void Disconnect(size_t hash) = 0;
			};

			struct __declspec(dllexport) Key {
				SignalInterface* signal;
			};
			using KeyPtr = std::shared_ptr<Key>;

			static inline size_t CalculateHash(const KeyPtr& key) {
				return std::hash<KeyPtr>{}(key);
			}

			explicit Connection(KeyPtr c) : key_{ std::move(c) } {

			}

			KeyPtr key_;
		};

		class __declspec(dllexport) ScopedConnection {
		public:
			ScopedConnection() = default;
			ScopedConnection(const Connection& connection) : connection_{ connection } {

			}

			/// @brief Assigns a new connection to the scoped connection, disconnecting the previous one.
			ScopedConnection& operator=(const Connection& connection);

			~ScopedConnection() {
				connection_.Disconnect();
			}

			ScopedConnection(const ScopedConnection&) = delete;
			ScopedConnection& operator=(const ScopedConnection&) = delete;

			ScopedConnection(ScopedConnection&&) = delete;
			ScopedConnection& operator=(ScopedConnection&&) = delete;

			void Disconnect();
			bool Connected() const;
			void Release();

		private:
			Connection connection_;
		};

		class __declspec(dllexport) ScopedConnections {
		public:
			ScopedConnections() = default;

			ScopedConnections(std::initializer_list<Connection> connections)
				: connections_(connections.begin(), connections.end()) {
			}

			~ScopedConnections() {
				Clear();
			}

			ScopedConnections(const ScopedConnections&) = delete;
			ScopedConnections& operator=(const ScopedConnections&) = delete;

			ScopedConnections(ScopedConnections&&) = delete;
			ScopedConnections& operator=(ScopedConnections&&) = delete;

			void operator+=(const Connection& connection);
			void operator+=(std::initializer_list<Connection> connections);
			void Clear();
			void CleanUp();
			int Size() const;

		private:
			std::vector<Connection> connections_;
		};

	}

	template <typename... Args> class __declspec(dllexport) Signal : public NahidaProject::SignalAndSlot::Connection::SignalInterface {
	public:
		using Callback = std::function<void(Args...)>;

		Signal() = default;
		~Signal();

		Signal(const Signal&) = delete;
		Signal& operator=(const Signal&) = delete;

		Signal(Signal&&) noexcept;
		Signal& operator=(Signal&&) noexcept;

		[[nodiscard]] NahidaProject::SignalAndSlot::Connection Connect(const Callback& callback);

		template <typename... Params> void operator()(Params&&... params);
		template <typename... Params> void Invoke(Params&&... params);
		template <typename T, typename... TArgs> [[nodiscard]] NahidaProject::SignalAndSlot::Connection Connect(T* object, void(T::* ptr)(TArgs... args)) {
			return Connect([object, ptr](Args... args) {
				(object->*ptr)(args...);
				});
		}

		void Clear();
		int Size() const noexcept;
		bool Empty() const noexcept;

	private:
		using KeyPtr = NahidaProject::SignalAndSlot::Connection::KeyPtr;

		void Disconnect(size_t hash) override;

		struct __declspec(dllexport) KeyCallback {
			KeyPtr key;
			Callback callback;
		};

		std::vector<KeyCallback> callbacks_;
	};

	template <typename Friend, typename... Args> class __declspec(dllexport) PublicSignal {
	public:
		using Callback = typename Signal<Args...>::Callback;
		friend Friend;

		[[nodiscard]] NahidaProject::SignalAndSlot::Connection Connect(const Callback& callback) {
			return signal_.Connect(callback);
		}

		template <typename T, typename... TArgs>
		NahidaProject::SignalAndSlot::Connection Connect(T* object, void(T::* ptr)(TArgs... args)) {
			return signal_.Connect(object, ptr);
		}

	private:
		PublicSignal() = default;
		PublicSignal(const PublicSignal&) = delete;
		PublicSignal& operator=(const PublicSignal&) = delete;
		PublicSignal(PublicSignal&&) noexcept = default;
		PublicSignal& operator=(PublicSignal&&) noexcept = default;

		template <typename... Params>
		void operator()(Params&&... params) {
			signal_.Invoke(std::forward<Params>(params)...);
		}

		template <typename... Params>
		void Invoke(Params&&... params) {
			signal_.Invoke(std::forward<Params>(params)...);
		}

		void Clear() {
			signal_.Clear();
		}

		int Size() const noexcept {
			return signal_.Size();
		}

		bool Empty() const noexcept {
			return signal_.Empty();
		}

		Signal<Args...> signal_;
	};

	template <typename... Args> Signal<Args...>::Signal::~Signal() {
		Clear();
	}

	template <typename... Args> Signal<Args...>::Signal(Signal<Args...>&& signal) noexcept : callbacks_{ std::move(signal.callbacks_) } {
		for (auto& keyCallback : callbacks_) {
			if (keyCallback.key) {
				keyCallback.key->signal = this;
			}
		}
		signal.Clear();
	}

	template <typename... Args> Signal<Args...>& Signal<Args...>::operator=(Signal<Args...>&& signal) noexcept {
		if (this != &signal) {
			callbacks_ = std::move(signal.callbacks_);

			for (auto& keyCallback : callbacks_) {
				if (keyCallback.key) {
					keyCallback.key->signal = this;
				}
			}
			signal.clear();
		}
		return *this;
	}

	template <typename... Args> NahidaProject::SignalAndSlot::Connection Signal<Args...>::Connect(const Callback& callback) {
		auto c = std::make_shared<NahidaProject::SignalAndSlot::Connection::Key>(this);
		callbacks_.push_back({ c, callback });
		return NahidaProject::SignalAndSlot::Connection(c);
	}

	template <typename... Args> template <typename... Params> void Signal<Args...>::operator()(Params&&... a) {
		Invoke(std::forward<Params>(a)...);
	}

	template <typename... Args> template <typename... Params> void Signal<Args...>::Invoke(Params&&... a) {
		const auto size = static_cast<int>(callbacks_.size());
		for (int i = 0; i < size && i < callbacks_.size(); ++i) {
			callbacks_[i].callback(a...);
		}
	}

	template <typename... Args> void Signal<Args...>::Clear() {
		for (auto& [key, callback] : callbacks_) {
			key->signal = nullptr;
		}
		callbacks_.clear();
	}

	template <typename... Args> int Signal<Args...>::Size() const noexcept {
		return static_cast<int>(callbacks_.size());
	}

	template <typename... Args> bool Signal<Args...>::Empty() const noexcept {
		return callbacks_.empty();
	}

	template <typename... Args> void Signal<Args...>::Disconnect(size_t hash) {
		auto it = std::find_if(callbacks_.begin(), callbacks_.end(), [hash](const KeyCallback& keyCallback) {
			if (NahidaProject::SignalAndSlot::Connection::CalculateHash(keyCallback.key) == hash) {
				return true;
			}
			return false;
		});
		if (it != callbacks_.end()) {
			it->key->signal = nullptr;
			callbacks_.erase(it);
		}
	}

}

#endif
